package next.fire.spinus.logx.provider.aliyun.impl;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import next.fire.spinus.logx.api.LogService;
import next.fire.spinus.logx.api.model.*;
import next.fire.spinus.logx.api.query.DateRange;
import next.fire.spinus.logx.api.query.LogDataQuery;
import next.fire.spinus.logx.base.AbstractClient;
import next.fire.spinus.logx.base.RequestHandler;
import next.fire.spinus.logx.provider.aliyun.client.AliyunHelper;
import next.fire.spinus.logx.provider.aliyun.client.AliyunLogClient;
import next.fire.spinus.logx.provider.aliyun.client.AliyunLogPbClient;
import next.fire.spinus.logx.provider.aliyun.conf.AliyunLogConfig;
import next.fire.spinus.logx.provider.aliyun.conf.AliyunLogConfigHolder;
import next.fire.spinus.logx.provider.aliyun.protobuf.LogItem;
import next.fire.spinus.logx.utils.http.FireHttpUtils;

import java.util.*;

/**
 * Created by daibing on 2020/9/14.
 */
public class AliyunLogServiceImpl implements LogService {
    private final AliyunLogClient logClient = new AliyunLogClient();
    private final AliyunLogPbClient logPbClient = new AliyunLogPbClient();

    @Override
    public ProviderEnum getProvider() {
        return ProviderEnum.ALIYUN;
    }

    /**
     * 请求签名
     * https://help.aliyun.com/document_detail/29012.html
     */
    @SafeVarargs
    @Override
    public final void loadConf(Map<String, Object>... params) {
        List<AliyunLogConfig> aliyunLogConfigList = new ArrayList<>();
        for (Map<String, Object> param : params) {
            JSONObject conf = (JSONObject) param;
            if (!ProviderEnum.ALIYUN.name().equalsIgnoreCase(conf.getString("provider"))) {
                continue;
            }
            aliyunLogConfigList.add(conf.toJavaObject(AliyunLogConfig.class));
        }
        if (aliyunLogConfigList.isEmpty()) {
            return;
        }
        AliyunLogConfigHolder.get().reload(aliyunLogConfigList);
    }

    /**
     * GetLogstore
     * https://help.aliyun.com/document_detail/29018.html
     */
    @Override
    public LogMeta getMeta(String logStoreCode) {
        final AliyunLogConfig config = AliyunLogConfigHolder.get().select(logStoreCode);
        if (config == null) {
            throw new RuntimeException("aliyun log config not found by " + logStoreCode);
        }
        String resourceUri = "/logstores/" + config.getLogStore();
        return logClient.execute(AbstractClient.Method.GET, resourceUri, logStoreCode, false, false, new RequestHandler<LogMeta>() {
            @Override
            public Map<String, Object> buildBizParams() {
                return null;
            }

            @Override
            public Map<String, Object> buildAccessParams() {
                JSONObject accessParams = new JSONObject();
                accessParams.put(AliyunLogClient.RESPONSE_DATA_LIST, false);
                return accessParams;
            }

            @Override
            public LogMeta extractData(Map<String, Object> output) {
                JSONObject data = (JSONObject) output;
                LogMeta meta = new LogMeta();
                meta.setLogStoreName(data.getString("logstoreName"));
                meta.setTtl(data.getInteger("ttl"));
                meta.setShardCount(data.getInteger("shardCount"));
                meta.setAutoSplit(data.getBoolean("autoSplit"));
                meta.setCreateTime(parseTimestamp(data.getLong("createTime")));
                meta.setLastModify(parseTimestamp(data.getLong("lastModifyTime")));
                return meta;
            }
        });
    }

    /**
     * GetIndex
     * https://help.aliyun.com/document_detail/74911.html
     */
    @Override
    public List<LogField> listField(String logStoreCode) {
        final AliyunLogConfig config = AliyunLogConfigHolder.get().select(logStoreCode);
        if (config == null) {
            throw new RuntimeException("aliyun log config not found by " + logStoreCode);
        }
        String resourceUri = "/logstores/" + config.getLogStore() + "/index";
        return logClient.execute(AbstractClient.Method.GET, resourceUri, logStoreCode, false, false, new RequestHandler<List<LogField>>() {
            @Override
            public Map<String, Object> buildBizParams() {
                return null;
            }

            @Override
            public Map<String, Object> buildAccessParams() {
                JSONObject accessParams = new JSONObject();
                accessParams.put(AliyunLogClient.RESPONSE_DATA_LIST, false);
                return accessParams;
            }

            @Override
            public List<LogField> extractData(Map<String, Object> output) {
                JSONObject data = (JSONObject) output;
                List<LogField> fields = new ArrayList<>();
                JSONObject keys = data.getJSONObject("keys");
                for (Map.Entry<String, Object> entry : keys.entrySet()) {
                    JSONObject key = (JSONObject) entry.getValue();
                    LogField field = new LogField();
                    field.setFieldName(entry.getKey());
                    field.setIndexMode(data.getString("index_mode"));
                    field.setDocValue(key.getBooleanValue("doc_value"));
                    field.setCaseSensitive(key.getBooleanValue("caseSensitive"));
                    field.setChn(key.getBooleanValue("chn"));
                    field.setAlias(key.getString("alias"));
                    field.setText("text".equalsIgnoreCase(key.getString("type")));
                    JSONArray token = key.getJSONArray("token");
                    if (token != null && !token.isEmpty()) {
                        field.setToken(token.toJavaList(String.class));
                    }
                    fields.add(field);
                }
                return fields;
            }
        });
    }

    /**
     * PutLogs
     * https://help.aliyun.com/document_detail/29026.html
     */
    @Override
    public String putData(String logStoreCode, final List<LogData> dataList) {
        if (dataList == null || dataList.isEmpty()) {
            return null;
        }
        final AliyunLogConfig config = AliyunLogConfigHolder.get().select(logStoreCode);
        if (config == null) {
            throw new RuntimeException("aliyun log config not found by " + logStoreCode);
        }
        String resourceUri = "/logstores/" + config.getLogStore() + "/shards/lb";
        return logPbClient.execute(AbstractClient.Method.POST, resourceUri, logStoreCode, false, false, new RequestHandler<String>() {
            @Override
            public Map<String, Object> buildBizParams() {
                List<LogItem> logItems = new ArrayList<>(dataList.size());
                for (LogData logData : dataList) {
                    LogItem logItem = new LogItem((int) (System.currentTimeMillis() / 1000));
                    logItem.PushBack("appId", logData.getAppId());
                    logItem.PushBack("stage", logData.getStage());
                    logItem.PushBack("level", logData.getLevel().name());
                    logItem.PushBack("logTime", String.valueOf(logData.getLogTime()));
                    logItem.PushBack("traceId", logData.getTraceId());
                    logItem.PushBack("frontendId", logData.getFrontendId());
                    logItem.PushBack("host", logData.getHost());
                    logItem.PushBack("thread", logData.getThread());
                    logItem.PushBack("location", logData.getLocation());
                    logItem.PushBack("message", logData.getMessage());
                    logItem.PushBack("throwable", logData.getThrowable());
                    logItems.add(logItem);
                }
                JSONObject data = new JSONObject();
                data.put(AliyunLogPbClient.LOG_ITEM_LIST, logItems);
                data.put(AliyunLogPbClient.LOG_TOPIC, dataList.get(0).getStage());
                data.put(AliyunLogPbClient.LOG_SOURCE, dataList.get(0).getHost());
                return data;
            }

            @Override
            public Map<String, Object> buildAccessParams() {
                JSONObject accessParams = new JSONObject();
                accessParams.put(AliyunLogClient.RESPONSE_DATA_LIST, false);
                return accessParams;
            }

            @Override
            public String extractData(Map<String, Object> output) {
                JSONObject data = (JSONObject) output;
                return data.getString(AliyunLogClient.LOG_REQUEST_ID);
            }
        });
    }

    /**
     * GetLogs
     * https://help.aliyun.com/document_detail/29029.html
     */
    @Override
    public List<LogData> listData(String logStoreCode, LogDataQuery query, int pageNo, int pageSize) {
        final AliyunLogConfig config = AliyunLogConfigHolder.get().select(logStoreCode);
        if (config == null) {
            throw new RuntimeException("aliyun log config not found by " + logStoreCode);
        }
        Map<String, String> queryString = new TreeMap<>();
        queryString.put("type", "log");
        queryString.put("topic", query.getStage());
        // 注意from和to两个字段为必填，并且为日志数据到达服务端的时间点，精度为秒，所以将结束时间调整为查询日志结束时间+3分钟
        queryString.put("from", String.valueOf(query.getDateRange().getStartTime().getTime() / 1000));
        queryString.put("to", String.valueOf(query.getDateRange().getEndTime().getTime() / 1000 + 3 * 60));
        queryString.put("query", this.buildQuery(query));
        queryString.put("line", String.valueOf(pageSize));
        // 注意从第一页开始
        queryString.put("offset", String.valueOf((pageNo - 1) * pageSize));
        queryString.put("reverse", String.valueOf(query.isReverse()));
        String resourceUri = AliyunHelper.getCanonicalizedResource("/logstores/" + config.getLogStore(), queryString);
        return logClient.execute(AbstractClient.Method.GET, resourceUri, logStoreCode, false, false, new RequestHandler<List<LogData>>() {
            @Override
            public Map<String, Object> buildBizParams() {
                return null;
            }

            @Override
            public Map<String, Object> buildAccessParams() {
                JSONObject accessParams = new JSONObject();
                accessParams.put(AliyunLogClient.RESPONSE_DATA_LIST, true);
                return accessParams;
            }

            @Override
            public List<LogData> extractData(Map<String, Object> output) {
                JSONObject data = (JSONObject) output;
                return data.getJSONArray(FireHttpUtils.RESPONSE_BODY).toJavaList(LogData.class);
            }
        });
    }

    @Override
    public OffsetResult<LogData> listDataByOffset(String logStoreCode, LogDataQuery query, String offsetPosition, int offsetSize) {
        throw new UnsupportedOperationException("Aliyun log query should by listDataByPage!");
    }

    @Override
    public List<String> listAppId(String logStoreCode, String stage) {
        LogDataQuery query = new LogDataQuery();
        query.setStage(stage);
        query.setDateRange(new DateRange(new Date(),  24 * 60, true));
        query.setQueryExpression("* | select distinct(appId) limit 600");
        List<LogData> logDataList = listData(logStoreCode, query, 1, 600);
        List<String> appIds = new ArrayList<>(logDataList.size());
        for (LogData logData : logDataList) {
            appIds.add(logData.getAppId());
        }
        return appIds;
    }

    private String buildQuery(LogDataQuery query) {
        String operator = " and ";
        StringBuilder sb = new StringBuilder();

        // 2025/1/3 新增多个appId查询的条件
        List<String> appIds = query.getAppIds();
        if (appIds != null && !appIds.isEmpty()) {
            sb.append("(");
            for (int i = 0; i < appIds.size(); i++) {
                sb.append("appId:\"").append(appIds.get(i)).append("\"");
                if (i < appIds.size() - 1) {
                    sb.append(" OR ");
                } else if (i == appIds.size() - 1) {
                    sb.append(") ").append(operator);
                }
            }
        } else if (!this.isBlank(query.getAppId())) {
            sb.append("appId:\"").append(query.getAppId()).append("\"").append(operator);
        }

        // 日志级别需要向上回溯，比如选择INFO级别可以看到INFO和INFO以上的日志，包括WARN, ERROR
        if (query.getLevel() != null && query.getLevel() != LevelEnum.ALL) {
            String levelQuery = this.buildLevelQuery(query.getLevel());
            sb.append(" (").append(levelQuery).append(")").append(operator);
        }
        // 时间区间遵循“左闭右开”原则
        if (query.getDateRange() != null) {
            sb.append("logTime in [")
                    .append(query.getDateRange().getStartTime().getTime()).append(" ")
                    .append(query.getDateRange().getEndTime().getTime())
                    .append(")").append(operator);
        }
        if (!this.isBlank(query.getTraceId())) {
            sb.append("traceId:\"").append(query.getTraceId()).append("\"").append(operator);
        }
        if (!this.isBlank(query.getFrontendId())) {
            sb.append("frontendId:\"").append(query.getFrontendId()).append("\"").append(operator);
        }
        if (!this.isBlank(query.getHost())) {
            sb.append("host:\"").append(query.getHost()).append("\"").append(operator);
        }
        if (!this.isBlank(query.getThread())) {
            sb.append("thread:\"").append(query.getThread()).append("\"").append(operator);
        }
        if (!this.isBlank(query.getLocation())) {
            sb.append("location:\"").append(query.getLocation()).append("\"").append(operator);
        }
        if (!this.isBlank(query.getMessage())) {
            sb.append("message:\"").append(query.getMessage()).append("\"").append(operator);
        }
        if (!this.isBlank(query.getThrowable())) {
            sb.append("throwable:\"").append(query.getThrowable()).append("\"").append(operator);
        }
        // 前端自己定义查询表达式
        if (!this.isBlank(query.getQueryExpression())) {
            sb.append(query.getQueryExpression()).append(operator);
        }
        return sb.substring(0, sb.length() - operator.length());
    }

    private String buildLevelQuery(LevelEnum level) {
        if (LevelEnum.ALL == level) {
            return null;
        }
        boolean matched = false;
        String operator = " or ";
        StringBuilder sb = new StringBuilder();
        for (LevelEnum value : LevelEnum.values()) {
            if (value == level) {
                matched = true;
            }
            if (matched) {
                sb.append("level:\"").append(value).append("\"").append(operator);
            }
        }
        return sb.substring(0, sb.length() - operator.length());
    }

    private Date parseTimestamp(Long timestamp) {
        if (timestamp.toString().length() == 13) {
            return new Date(timestamp);
        }
        return new Date(timestamp.intValue() * 1000L);
    }

    protected boolean isBlank(String s) {
        return s == null || s.trim().length() == 0;
    }
}
